<template>
	<div :class="wrapStyleClasses">
		<div v-if="isLoading" class="vgt-loading vgt-center-align">
			<slot name="loadingContent">
				<span class="vgt-loading__content"> Loading... </span>
			</slot>
		</div>
		<div class="vgt-inner-wrap" :class="{ 'is-loading': isLoading }">
			<slot
				v-if="paginate && paginateOnTop"
				name="pagination-top"
				:pageChanged="pageChanged"
				:perPageChanged="perPageChanged"
				:total="totalRows || totalRowCount"
			>
				<vgt-pagination
					ref="paginationTop"
					@page-changed="pageChanged"
					@per-page-changed="perPageChanged"
					:perPage="perPage"
					:rtl="rtl"
					:total="totalRows || totalRowCount"
					:mode="paginationMode"
					:nextText="nextText"
					:prevText="prevText"
					:rowsPerPageText="rowsPerPageText"
					:perPageDropdownEnabled="paginationOptions.perPageDropdownEnabled"
					:customRowsPerPageDropdown="customRowsPerPageDropdown"
					:paginateDropdownAllowAll="paginateDropdownAllowAll"
					:ofText="ofText"
					:pageText="pageText"
					:allText="allText"
					:info-fn="paginationInfoFn"
				></vgt-pagination>
			</slot>
			<vgt-global-search
				v-on:keyup="searchTableOnKeyUp"
				v-on:enter="searchTableOnEnter"
				:value="globalSearchTerm"
				@input="globalSearchTerm = $event"
				:search-enabled="searchEnabled && externalSearchQuery == null"
				:global-search-placeholder="searchPlaceholder"
			>
				<template #internal-table-actions v-if="$slots['table-actions']">
					<slot name="table-actions"> </slot>
				</template>
			</vgt-global-search>
			<div
				v-if="selectedRowCount && !disableSelectInfo"
				class="vgt-selection-info-row clearfix"
				:class="selectionInfoClass"
			>
				{{ selectionInfo }}
				<a href="" @click.prevent="unselectAllInternal(true)">
					{{ clearSelectionText }}
				</a>
				<div class="vgt-selection-info-row__actions vgt-pull-right">
					<slot name="selected-row-actions"> </slot>
				</div>
			</div>
			<div class="vgt-fixed-header">
				<table id="vgt-table" v-if="fixedHeader" :class="tableStyleClasses">
					<colgroup>
						<col
							v-for="(column, index) in columns"
							:key="index"
							:id="`col-${index}`"
						/>
					</colgroup>
					<!-- Table header -->
					<vgt-table-header
						ref="table-header-secondary"
						v-on:toggle-select-all="toggleSelectAll"
						v-on:toggle-expand-rows-all="toggleExpandRowsAll"
						v-on:sort-change="changeSort"
						@filter-changed="filterRows"
						:columns="columns"
						:line-numbers="lineNumbers"
						:selectable="selectable"
						:all-selected="allSelected"
						:all-selected-indeterminate="allSelectedIndeterminate"
						:mode="mode"
						:sortable="sortable"
						:multiple-column-sort="multipleColumnSort"
						:typed-columns="typedColumns"
						:getClasses="getClasses"
						:searchEnabled="searchEnabled"
						:paginated="paginated"
						:table-ref="$refs.table"
					>
						<template #table-column="slotProps">
							<slot name="table-column" :column="slotProps.column">
								<span>{{ slotProps.column.label }}</span>
							</slot>
						</template>
						<template #column-filter="slotProps">
							<slot
								name="column-filter"
								:column="slotProps.column"
								:updateFilters="slotProps.updateFilters"
							></slot>
						</template>
					</vgt-table-header>
				</table>
			</div>
			<div :class="{ 'vgt-responsive': responsive }" :style="wrapperStyles">
				<table id="vgt-table" ref="table" :class="tableStyles">
					<colgroup>
						<col
							v-for="(column, index) in columns"
							:key="index"
							:id="`col-${index}`"
						/>
					</colgroup>
					<!-- Table header -->
					<vgt-table-header
						ref="table-header-primary"
						v-on:toggle-select-all="toggleSelectAll"
						v-on:toggle-expand-rows-all="toggleExpandRowsAll"
						v-on:sort-change="changeSort"
						@filter-changed="filterRows"
						:columns="columns"
						:line-numbers="lineNumbers"
						:selectable="selectable"
						:all-selected="allSelected"
						:all-selected-indeterminate="allSelectedIndeterminate"
						:mode="mode"
						:sortable="sortable"
						:multiple-column-sort="multipleColumnSort"
						:typed-columns="typedColumns"
						:getClasses="getClasses"
						:searchEnabled="searchEnabled"
					>
						<template #table-column="slotProps">
							<slot name="table-column" :column="slotProps.column">
								<span>{{ slotProps.column.label }}</span>
							</slot>
						</template>
						<template #column-filter="slotProps">
							<slot
								name="column-filter"
								:column="slotProps.column"
								:updateFilters="slotProps.updateFilters"
							></slot>
						</template>
					</vgt-table-header>

					<!-- Table body starts here -->
					<tbody v-for="(headerRow, hIndex) in paginated" :key="hIndex">
						<!-- if group row header is at the top -->
						<vgt-header-row
							v-if="groupHeaderOnTop"
							@vgtExpand="toggleExpand(headerRow[rowKeyField])"
							:header-row="headerRow"
							:columns="columns"
							:line-numbers="lineNumbers"
							:selectable="selectable"
							:select-all-by-group="selectAllByGroup"
							:collapsable="groupOptions.collapsable"
							:collect-formatted="collectFormatted"
							:formatted-row="formattedRow"
							:class="getRowStyleClass(headerRow)"
							:get-classes="getClasses"
							:full-colspan="fullColspan"
							:groupIndex="hIndex"
							v-on:select-group-change="toggleSelectGroup($event, headerRow)"
						>
							<template
								v-if="hasHeaderRowTemplate"
								#table-header-row="slotProps"
							>
								<slot
									name="table-header-row"
									:column="slotProps.column"
									:formattedRow="slotProps.formattedRow"
									:row="slotProps.row"
								>
								</slot>
							</template>
						</vgt-header-row>
						<!-- normal rows here. we loop over all rows -->
						<template v-for="(row, index) in headerRow.children">
							<tr
								v-if="groupOptions.collapsable ? headerRow.vgtIsExpanded : true"
								:key="row.originalIndex"
								:class="getRowStyleClass(row)"
								@mouseenter="onMouseenter(row, index)"
								@mouseleave="onMouseleave(row, index)"
								@dblclick="onRowDoubleClicked(row, index, $event)"
								@click="onRowClicked(row, index, $event)"
								@auxclick="onRowAuxClicked(row, index, $event)"
							>
								<th v-if="lineNumbers" class="line-numbers">
									{{ getCurrentIndex(row.originalIndex) }}
								</th>
								<th
									v-if="selectable"
									@click.stop="onCheckboxClicked(row, index, $event)"
									class="vgt-checkbox-col"
								>
									<input
										type="checkbox"
										:disabled="row.vgtDisabled"
										:checked="row.vgtSelected"
									/>
								</th>
								<template v-for="(column, i) in columns">
									<td
										:key="i"
										v-if="!column.hidden && column.field"
										@click="onCellClicked(row, column, index, $event)"
										:class="getClasses(i, 'td', row)"
										v-bind:data-label="compactMode ? column.label : undefined"
									>
										<slot
											name="table-row"
											:row="row"
											:column="column"
											:formattedRow="formattedRow(row)"
											:index="index"
											:expandedRow="expandedRowIndex === index"
										>
											<span v-if="!column.html">
												{{ collectFormatted(row, column) }}
											</span>
											<span v-else v-html="collect(row, column.field)"> </span>
										</slot>
									</td>
								</template>
							</tr>
							<tr
								v-if="expandedRowIndex === index"
								:class="expandedRowDetailClasses"
								:key="row.originalIndex"
							>
								<td :colspan="fullColspan">
									<slot
										name="row-details"
										:row="row"
										:formattedRow="formattedRow(row)"
										:index="index"
									>
									</slot>
								</td>
							</tr>
							<tr
								v-if="row['expanded']"
								:key="row.originalIndex"
							>
								<td :colspan="fullColspan">
									{{ row["expandedRow"] }}
								</td>
							</tr>
						</template>
						<!-- if group row header is at the bottom -->
						<vgt-header-row
							v-if="groupHeaderOnBottom"
							:header-row="headerRow"
							:columns="columns"
							:line-numbers="lineNumbers"
							:selectable="selectable"
							:select-all-by-group="selectAllByGroup"
							:collect-formatted="collectFormatted"
							:formatted-row="formattedRow"
							:get-classes="getClasses"
							:full-colspan="fullColspan"
							:groupIndex="index"
							v-on:select-group-change="toggleSelectGroup($event, headerRow)"
						>
							<template
								v-if="hasHeaderRowTemplate"
								#table-header-row="slotProps"
							>
								<slot
									name="table-header-row"
									:column="slotProps.column"
									:formattedRow="slotProps.formattedRow"
									:row="slotProps.row"
								>
								</slot>
							</template>
						</vgt-header-row>
					</tbody>

					<tbody v-if="showEmptySlot">
						<tr>
							<td :colspan="fullColspan">
								<slot name="emptystate">
									<div class="vgt-center-align vgt-text-disabled">
										No data for table
									</div>
								</slot>
							</td>
						</tr>
					</tbody>
				</table>
			</div>
			<div v-if="hasFooterSlot" class="vgt-wrap__actions-footer">
				<slot name="table-actions-bottom"> </slot>
			</div>
			<slot
				v-if="paginate && paginateOnBottom"
				name="pagination-bottom"
				:pageChanged="pageChanged"
				:perPageChanged="perPageChanged"
				:total="totalRows || totalRowCount"
			>
				<vgt-pagination
					ref="paginationBottom"
					@page-changed="pageChanged"
					@per-page-changed="perPageChanged"
					:perPage="perPage"
					:rtl="rtl"
					:total="totalRows || totalRowCount"
					:mode="paginationMode"
					:nextText="nextText"
					:prevText="prevText"
					:rowsPerPageText="rowsPerPageText"
					:perPageDropdownEnabled="paginationOptions.perPageDropdownEnabled"
					:customRowsPerPageDropdown="customRowsPerPageDropdown"
					:paginateDropdownAllowAll="paginateDropdownAllowAll"
					:ofText="ofText"
					:pageText="pageText"
					:allText="allText"
					:info-fn="paginationInfoFn"
				></vgt-pagination>
			</slot>
		</div>
	</div>
</template>

<script>
import { DEFAULT_SORT_TYPE, SORT_TYPES } from "./utils/constants";
import isEqual from "lodash.isequal";
import defaultType from "./types/default";
import VgtPagination from "./pagination/VgtPagination.vue";
import VgtGlobalSearch from "./VgtGlobalSearch.vue";
import VgtTableHeader from "./VgtTableHeader.vue";
import VgtHeaderRow from "./VgtHeaderRow.vue";

// here we load each data type module.
import * as CoreDataTypes from "./types/index";

const dataTypes = {};
const coreDataTypes = CoreDataTypes.default;
Object.keys(coreDataTypes).forEach((key) => {
	const compName = key.replace(/^\.\//, "").replace(/\.js/, "");
	dataTypes[compName] = coreDataTypes[key].default;
});

export default {
	name: "vue-good-table",
	props: {
		isLoading: { default: null, type: Boolean },
		maxHeight: { default: null, type: String },
		fixedHeader: Boolean,
		theme: { default: "" },
		mode: { default: "local" }, // could be remote
		totalRows: {}, // required if mode = 'remote'
		styleClass: { default: "vgt-table bordered" },
		columns: {},
		rows: {},
		lineNumbers: Boolean,
		responsive: { default: true, type: Boolean },
		rtl: Boolean,
		rowStyleClass: { default: null, type: [Function, String] },
		compactMode: Boolean,
		enableRowExpand: { default: false, type: Boolean },

		expandRowsOptions: {
			default() {
				return {
					enabled: false,
				};
			},
		},

		groupOptions: {
			default() {
				return {
					enabled: false,
					collapsable: false,
					rowKey: null,
				};
			},
		},

		selectOptions: {
			default() {
				return {
					enabled: false,
					selectionInfoClass: "",
					selectionText: "rows selected",
					clearSelectionText: "clear",
					disableSelectInfo: false,
					selectAllByGroup: false,
				};
			},
		},

		// sort
		sortOptions: {
			default() {
				return {
					enabled: true,
					multipleColumns: true,
					initialSortBy: {},
				};
			},
		},

		// pagination
		paginationOptions: {
			default() {
				return {
					enabled: false,
					position: "bottom",
					perPage: 10,
					perPageDropdown: null,
					perPageDropdownEnabled: true,
					dropdownAllowAll: true,
					mode: "records", // or pages
					infoFn: null,
				};
			},
		},

		searchOptions: {
			default() {
				return {
					enabled: false,
					trigger: null,
					externalQuery: null,
					searchFn: null,
					placeholder: "Search Table",
				};
			},
		},

		expandedRowClasses: {
			default: "",
			type: String,
		},
		expandedRowDetailClasses: {
			default: "",
			type: String,
		},
	},

	data: () => ({
		// loading state for remote mode
		tableLoading: false,

		// text options
		nextText: "Next",
		prevText: "Previous",
		rowsPerPageText: "Rows per page",
		ofText: "of",
		allText: "All",
		pageText: "page",

		// internal select options
		selectable: false,
		selectOnCheckboxOnly: false,
		selectAllByPage: true,
		disableSelectInfo: false,
		selectionInfoClass: "",
		selectionText: "rows selected",
		clearSelectionText: "clear",

		// keys for rows that are currently expanded
		maintainExpanded: true,
		expandedRowKeys: new Set(),

		// internal sort options
		sortable: true,
		defaultSortBy: null,
		multipleColumnSort: true,

		// internal search options
		searchEnabled: false,
		searchTrigger: null,
		externalSearchQuery: null,
		searchFn: null,
		searchPlaceholder: "Search Table",
		searchSkipDiacritics: false,

		// internal pagination options
		perPage: null,
		paginate: false,
		paginateOnTop: false,
		paginateOnBottom: true,
		customRowsPerPageDropdown: [],
		paginateDropdownAllowAll: true,
		paginationMode: "records",
		paginationInfoFn: null,

		currentPage: 1,
		currentPerPage: 10,
		sorts: [],
		globalSearchTerm: "",
		filteredRows: [],
		columnFilters: {},
		forceSearch: false,
		sortChanged: false,
		dataTypes: dataTypes || {},

		expandedRowIndex: null,
	}),

	emits: [
		"select-all",
		"selected-rows-change",
		"search",
		"per-page-change",
		"page-change",
		"update:isLoading",
		"sort-change",
		"row-click",
		"row-dblclick",
		"row-aux-click",
		"cell-click",
		"row-mouseenter",
		"row-mouseleave",
		"column-filter",
	],

	watch: {
		rows: {
			handler() {
				this.$emit("update:isLoading", false);
				this.filterRows(this.columnFilters, false);
			},
			deep: true,
			immediate: true,
		},

		selectOptions: {
			handler() {
				this.initializeSelect();
			},
			deep: true,
			immediate: true,
		},

		paginationOptions: {
			handler(newValue, oldValue) {
				if (!isEqual(newValue, oldValue)) {
					this.initializePagination();
				}
			},
			deep: true,
			immediate: true,
		},

		expandRowsOptions: {
			handler(newValue, oldValue) {
				this.initializeExpandRows();
			},
			deep: true,
			immediate: true,
		},

		searchOptions: {
			handler() {
				if (
					this.searchOptions.externalQuery !== undefined &&
					this.searchOptions.externalQuery !== this.searchTerm
				) {
					//* we need to set searchTerm to externalQuery first.
					this.externalSearchQuery = this.searchOptions.externalQuery;
					this.handleSearch();
				}
				this.initializeSearch();
			},
			deep: true,
			immediate: true,
		},

		sortOptions: {
			handler(newValue, oldValue) {
				if (!isEqual(newValue, oldValue)) {
					this.initializeSort();
				}
			},
			deep: true,
		},

		selectedRows(newValue, oldValue) {
			if (!isEqual(newValue, oldValue)) {
				this.$emit("selected-rows-change", {
					selectedRows: this.selectedRows,
				});
			}
		},
	},

	computed: {
		tableStyles() {
			if (this.compactMode) return this.tableStyleClasses + "vgt-compact";
			else return this.tableStyleClasses;
		},
		hasFooterSlot() {
			return !!this.$slots["table-actions-bottom"];
		},
		wrapperStyles() {
			return {
				overflow: "scroll-y",
				maxHeight: this.maxHeight ? this.maxHeight : "auto",
			};
		},

		rowKeyField() {
			return this.groupOptions.rowKey || "vgt_header_id";
		},

		hasHeaderRowTemplate() {
			return !!this.$slots["table-header-row"];
		},

		showEmptySlot() {
			if (!this.paginated.length) return true;

			if (
				this.paginated[0].label === "no groups" &&
				!this.paginated[0].children.length
			) {
				return true;
			}

			return false;
		},

		allSelected() {
			return (
				this.selectedRowCount > 0 &&
				((this.selectAllByPage &&
					this.selectedPageRowsCount === this.totalPageRowCount) ||
					(!this.selectAllByPage &&
						this.selectedRowCount === this.totalRowCount))
			);
		},

		allSelectedIndeterminate() {
			return (
				!this.allSelected &&
				((this.selectAllByPage && this.selectedPageRowsCount > 0) ||
					(!this.selectAllByPage && this.selectedRowCount > 0))
			);
		},

		selectionInfo() {
			return `${this.selectedRowCount} ${this.selectionText}`;
		},

		selectedRowCount() {
			return this.selectedRows.length;
		},

		selectedPageRowsCount() {
			return this.selectedPageRows.length;
		},

		selectedPageRows() {
			const selectedRows = [];
			this.paginated.forEach((headerRow) => {
				headerRow.children.forEach((row) => {
					if (row.vgtSelected) {
						selectedRows.push(row);
					}
				});
			});
			return selectedRows;
		},

		selectedRows() {
			const selectedRows = [];
			this.processedRows.forEach((headerRow) => {
				headerRow.children.forEach((row) => {
					if (row.vgtSelected) {
						selectedRows.push(row);
					}
				});
			});
			return selectedRows.sort((r1, r2) => r1.originalIndex - r2.originalIndex);
		},

		fullColspan() {
			let fullColspan = 0;
			for (let i = 0; i < this.columns.length; i += 1) {
				if (!this.columns[i].hidden) {
					fullColspan += 1;
				}
			}
			if (this.lineNumbers) fullColspan++;
			if (this.selectable) fullColspan++;
			if (this.expandRowsEnabled) fullColspan++;
			return fullColspan;
		},
		groupHeaderOnTop() {
			if (
				this.groupOptions &&
				this.groupOptions.enabled &&
				this.groupOptions.headerPosition &&
				this.groupOptions.headerPosition === "bottom"
			) {
				return false;
			}
			if (this.groupOptions && this.groupOptions.enabled) return true;

			// will only get here if groupOptions is false
			return false;
		},
		groupHeaderOnBottom() {
			if (
				this.groupOptions &&
				this.groupOptions.enabled &&
				this.groupOptions.headerPosition &&
				this.groupOptions.headerPosition === "bottom"
			) {
				return true;
			}
			return false;
		},
		totalRowCount() {
			const total = this.processedRows.reduce((total, headerRow) => {
				const childrenCount = headerRow.children
					? headerRow.children.length
					: 0;
				return total + childrenCount;
			}, 0);
			return total;
		},
		totalPageRowCount() {
			const total = this.paginated.reduce((total, headerRow) => {
				const childrenCount = headerRow.children
					? headerRow.children.length
					: 0;
				return total + childrenCount;
			}, 0);
			return total;
		},
		wrapStyleClasses() {
			let classes = "vgt-wrap";
			if (this.rtl) classes += " rtl";
			classes += ` ${this.theme}`;
			return classes;
		},
		tableStyleClasses() {
			let classes = this.styleClass;
			classes += ` ${this.theme}`;
			return classes;
		},

		searchTerm() {
			return this.externalSearchQuery != null
				? this.externalSearchQuery
				: this.globalSearchTerm;
		},

		//
		globalSearchAllowed() {
			if (
				this.searchEnabled &&
				!!this.globalSearchTerm &&
				this.searchTrigger !== "enter"
			) {
				return true;
			}

			if (this.externalSearchQuery != null && this.searchTrigger !== "enter") {
				return true;
			}

			if (this.forceSearch) {
				this.forceSearch = false;
				return true;
			}

			return false;
		},

		// this is done everytime sortColumn
		// or sort type changes
		//----------------------------------------
		processedRows() {
			// we only process rows when mode is local
			let computedRows = this.filteredRows;
			if (this.mode === "remote") {
				return computedRows;
			}

			// take care of the global filter here also
			if (this.globalSearchAllowed) {
				// here also we need to de-construct and then
				// re-construct the rows.
				const allRows = [];
				this.filteredRows.forEach((headerRow) => {
					allRows.push(...headerRow.children);
				});
				const filteredRows = [];
				allRows.forEach((row) => {
					for (let i = 0; i < this.columns.length; i += 1) {
						const col = this.columns[i];
						// if col does not have search disabled,
						if (!col.globalSearchDisabled) {
							// if a search function is provided,
							// use that for searching, otherwise,
							// use the default search behavior
							if (this.searchFn) {
								const foundMatch = this.searchFn(
									row,
									col,
									this.collectFormatted(row, col),
									this.searchTerm
								);
								if (foundMatch) {
									filteredRows.push(row);
									break; // break the loop
								}
							} else {
								// comparison
								const matched = defaultType.filterPredicate(
									this.collectFormatted(row, col),
									this.searchTerm,
									this.searchSkipDiacritics
								);
								if (matched) {
									filteredRows.push(row);
									break; // break loop
								}
							}
						}
					}
				});

				// this is where we emit on search
				this.$emit("search", {
					searchTerm: this.searchTerm,
					rowCount: filteredRows.length,
				});

				// here we need to reconstruct the nested structure
				// of rows
				computedRows = [];
				this.filteredRows.forEach((headerRow) => {
					const i = headerRow.vgt_header_id;
					const children = filteredRows.filter((r) => r.vgt_id === i);
					if (children.length) {
						const newHeaderRow = JSON.parse(JSON.stringify(headerRow));
						newHeaderRow.children = children;
						computedRows.push(newHeaderRow);
					}
				});
			}
			if (this.sorts.length) {
				//* we need to sort
				computedRows.forEach((cRows) => {
					cRows.children.sort((xRow, yRow) => {
						//* we need to get column for each sort
						let sortValue;
						for (let i = 0; i < this.sorts.length; i += 1) {
							const srt = this.sorts[i];

							if (srt.type === SORT_TYPES.None) {
								//* if no sort, we need to use the original index to sort.
								sortValue =
									sortValue || xRow.originalIndex - yRow.originalIndex;
							} else {
								const column = this.getColumnForField(srt.field);
								const xvalue = this.collect(xRow, srt.field);
								const yvalue = this.collect(yRow, srt.field);

								//* if a custom sort function has been provided we use that
								const { sortFn } = column;
								if (sortFn && typeof sortFn === "function") {
									sortValue =
										sortValue ||
										sortFn(xvalue, yvalue, column, xRow, yRow) *
											(srt.type === SORT_TYPES.Descending ? -1 : 1);
								} else {
									//* else we use our own sort
									sortValue =
										sortValue ||
										column.typeDef.compare(xvalue, yvalue, column) *
											(srt.type === SORT_TYPES.Descending ? -1 : 1);
								}
							}
						}
						return sortValue;
					});
				});
			}

			// if the filtering is event based, we need to maintain filter
			// rows
			if (this.searchTrigger === "enter") {
				this.filteredRows = computedRows;
			}

			return computedRows;
		},

		paginated() {
			if (!this.processedRows.length) return [];

			if (this.mode === "remote") {
				return this.processedRows;
			}

			//* flatten the rows for paging.
			let paginatedRows = [];
			this.processedRows.forEach((childRows) => {
				//* only add headers when group options are enabled.
				if (this.groupOptions.enabled) {
					paginatedRows.push(childRows);
				}
				paginatedRows.push(...childRows.children);
			});

			if (this.paginate) {
				let pageStart = (this.currentPage - 1) * this.currentPerPage;

				// in case of filtering we might be on a page that is
				// not relevant anymore
				// also, if setting to all, current page will not be valid
				if (pageStart >= paginatedRows.length || this.currentPerPage === -1) {
					this.currentPage = 1;
					pageStart = 0;
				}

				// calculate page end now
				let pageEnd = paginatedRows.length + 1;

				// if the setting is set to 'all'
				if (this.currentPerPage !== -1) {
					pageEnd = this.currentPage * this.currentPerPage;
				}

				paginatedRows = paginatedRows.slice(pageStart, pageEnd);
			}
			// reconstruct paginated rows here
			const reconstructedRows = [];
			paginatedRows.forEach((flatRow) => {
				//* header row?
				if (flatRow.vgt_header_id !== undefined) {
					this.handleExpanded(flatRow);
					const newHeaderRow = JSON.parse(JSON.stringify(flatRow));
					newHeaderRow.children = [];
					reconstructedRows.push(newHeaderRow);
				} else {
					//* child row
					let hRow = reconstructedRows.find(
						(r) => r.vgt_header_id === flatRow.vgt_id
					);
					if (!hRow) {
						hRow = this.processedRows.find(
							(r) => r.vgt_header_id === flatRow.vgt_id
						);
						if (hRow) {
							hRow = JSON.parse(JSON.stringify(hRow));
							hRow.children = [];
							reconstructedRows.push(hRow);
						}
					}
					hRow.children.push(flatRow);
				}
			});
			return reconstructedRows;
		},

		originalRows() {
			const rows = JSON.parse(JSON.stringify(this.rows));
			let nestedRows = [];
			if (!this.groupOptions.enabled) {
				nestedRows = this.handleGrouped([
					{
						label: "no groups",
						children: rows,
					},
				]);
			} else {
				nestedRows = this.handleGrouped(rows);
			}
			// we need to preserve the original index of
			// rows so lets do that
			let index = 0;
			nestedRows.forEach((headerRow) => {
				headerRow.children.forEach((row) => {
					row.originalIndex = index++;
				});
			});

			return nestedRows;
		},

		typedColumns() {
			const columns = this.columns;
			for (let i = 0; i < this.columns.length; i++) {
				const column = columns[i];
				column.typeDef = this.dataTypes[column.type] || defaultType;
			}
			return columns;
		},

		hasRowClickListener() {
			return this.$attrs && this.$attrs["row-click"];
		},
	},

	methods: {
		//* we need to check for expanded row state here
		//* to maintain it when sorting/filtering
		handleExpanded(headerRow) {
			if (
				this.maintainExpanded &&
				this.expandedRowKeys.has(headerRow[this.rowKeyField])
			) {
				headerRow["vgtIsExpanded"] = true;
			} else {
				headerRow["vgtIsExpanded"] = false;
			}
		},
		toggleExpand(id) {
			const headerRow = this.filteredRows.find(
				(r) => r[this.rowKeyField] === id
			);
			if (headerRow) {
				headerRow["vgtIsExpanded"] = !headerRow.vgtIsExpanded;
			}
			if (this.maintainExpanded && headerRow.vgtIsExpanded) {
				this.expandedRowKeys.add(headerRow[this.rowKeyField]);
			} else {
				this.expandedRowKeys.delete(headerRow[this.rowKeyField]);
			}
		},

		expandAll() {
			this.filteredRows.forEach((row) => {
				row["vgtIsExpanded"] = true;
				if (this.maintainExpanded) {
					this.expandedRowKeys.add(row[this.rowKeyField]);
				}
			});
		},

		collapseAll() {
			this.filteredRows.forEach((row) => {
				row["vgtIsExpanded"] = false;
				this.expandedRowKeys.clear();
			});
		},

		getColumnForField(field) {
			for (let i = 0; i < this.typedColumns.length; i += 1) {
				if (this.typedColumns[i].field === field) return this.typedColumns[i];
			}
		},

		handleSearch() {
			this.resetTable();
			// for remote mode, we need to emit search
			if (this.mode === "remote") {
				this.$emit("search", {
					searchTerm: this.searchTerm,
				});
			}
		},

		reset() {
			this.initializeSort();
			this.changePage(1);
			this.$refs["table-header-primary"].reset(true);
			if (this.$refs["table-header-secondary"]) {
				this.$refs["table-header-secondary"].reset(true);
			}
		},

		emitSelectedRows() {
			this.$emit("select-all", {
				selected: this.selectedRowCount === this.totalRowCount,
				selectedRows: this.selectedRows,
			});
		},

		unselectAllInternal(forceAll) {
			const rows =
				this.selectAllByPage && !forceAll ? this.paginated : this.filteredRows;
			rows.forEach((headerRow, i) => {
				headerRow.children.forEach((row, j) => {
					row["vgtSelected"] = false;
				});
			});
			this.emitSelectedRows();
		},

		toggleSelectAll() {
			if (this.allSelected) {
				this.unselectAllInternal();
				return;
			}
			const rows = this.selectAllByPage ? this.paginated : this.filteredRows;
			rows.forEach((headerRow) => {
				headerRow.children.forEach((row) => {
					row["vgtSelected"] = true;
				});
			});
			this.emitSelectedRows();
		},

		toggleExpandRowsAll() {
			for (let row of this.rows) {
				if (row["expandedRow"]) {
					row["expanded"] = !row["expanded"];
				} else {
					row["expanded"] = false;
				}
			}
			this.$emit("toggle-expand-rows-all", {});
		},

		toggleSelectGroup(event, headerRow) {
			headerRow.children.forEach((row) => {
				row["vgtSelected"] = event;
			});
		},

		changePage(value) {
			const enabled = this.paginate;
			let { paginationBottom, paginationTop } = this.$refs;
			if (enabled) {
				if (this.paginateOnTop && paginationTop) {
					paginationTop.currentPage = value;
				}
				if (this.paginateOnBottom && paginationBottom) {
					paginationBottom.currentPage = value;
				}
				// we also need to set the currentPage
				// for table.
				this.currentPage = value;
			}
		},

		pageChangedEvent() {
			return {
				currentPage: this.currentPage,
				currentPerPage: this.currentPerPage,
				total: Math.floor(this.totalRowCount / this.currentPerPage),
			};
		},

		pageChanged(pagination) {
			this.currentPage = pagination.currentPage;
			if (!pagination.noEmit) {
				const pageChangedEvent = this.pageChangedEvent();
				pageChangedEvent.prevPage = pagination.prevPage;
				this.$emit("page-change", pageChangedEvent);
				if (this.mode === "remote") {
					this.$emit("update:isLoading", true);
				}
			}
		},

		perPageChanged(pagination) {
			this.currentPerPage = pagination.currentPerPage;
			// ensure that both sides of pagination are in agreement
			// this fixes changes during position = 'both'
			let paginationPosition = this.paginationOptions.position;
			if (
				this.$refs.paginationTop &&
				(paginationPosition === "top" || paginationPosition === "both")
			) {
				this.$refs.paginationTop.currentPerPage = this.currentPerPage;
			}
			if (
				this.$refs.paginationBottom &&
				(paginationPosition === "bottom" || paginationPosition === "both")
			) {
				this.$refs.paginationBottom.currentPerPage = this.currentPerPage;
			}
			//* update perPage also
			const perPageChangedEvent = this.pageChangedEvent();
			this.$emit("per-page-change", perPageChangedEvent);
			if (this.mode === "remote") {
				this.$emit("update:isLoading", true);
			}
		},

		changeSort(sorts) {
			this.sorts = sorts;
			this.$emit("sort-change", sorts);

			// every time we change sort we need to reset to page 1
			this.changePage(1);

			// if the mode is remote, we don't need to do anything
			// after this. just set table loading to true
			if (this.mode === "remote") {
				this.$emit("update:isLoading", true);
				return;
			}
			this.sortChanged = true;
		},

		toggleRowExpand(row, index) {
			if (this.expandedRowIndex === index) {
				this.expandedRowIndex = null;
			} else {
				this.expandedRowIndex = index;
			}
		},

		// checkbox click should always do the following
		onCheckboxClicked(row, index, event) {
			if (this.enableRowExpand) {
				this.toggleRowExpand(row, index);
			}
			row["vgtSelected"] = !row.vgtSelected;
			this.$emit("row-click", {
				row,
				pageIndex: index,
				selected: !!row.vgtSelected,
				event,
			});
		},

		toggleExpandRow(row) {
			row["expanded"] = !row["expanded"];
		},

		onRowDoubleClicked(row, index, event) {
			this.$emit("row-dblclick", {
				row,
				pageIndex: index,
				selected: !!row.vgtSelected,
				event,
			});
		},

		onRowClicked(row, index, event) {
			if (this.enableRowExpand) {
				this.toggleRowExpand(row, index);
			}
			if (this.selectable && !this.selectOnCheckboxOnly) {
				row["vgtSelected"] = !row.vgtSelected;
			}
			this.$emit("row-click", {
				row,
				pageIndex: index,
				selected: !!row.vgtSelected,
				event,
			});
		},

		onRowAuxClicked(row, index, event) {
			this.$emit("row-aux-click", {
				row,
				pageIndex: index,
				selected: !!row.vgtSelected,
				event,
			});
		},

		onCellClicked(row, column, rowIndex, event) {
			this.$emit("cell-click", {
				row,
				column,
				rowIndex,
				event,
			});
		},

		onMouseenter(row, index) {
			this.$emit("row-mouseenter", {
				row,
				pageIndex: index,
			});
		},

		onMouseleave(row, index) {
			this.$emit("row-mouseleave", {
				row,
				pageIndex: index,
			});
		},

		searchTableOnEnter() {
			if (this.searchTrigger === "enter") {
				this.handleSearch();
				// we reset the filteredRows here because
				// we want to search across everything.
				this.filteredRows = JSON.parse(JSON.stringify(this.originalRows));
				this.forceSearch = true;
				this.sortChanged = true;
			}
		},

		searchTableOnKeyUp() {
			if (this.searchTrigger !== "enter") {
				this.handleSearch();
			}
		},

		resetTable() {
			this.unselectAllInternal(true);
			// every time we searchTable
			this.changePage(1);
		},

		// field can be:
		// 1. function (passed as a string using function.name. For example: 'bound myFunction')
		// 2. regular property - ex: 'prop'
		// 3. nested property path - ex: 'nested.prop'
		collect(obj, field) {
			// utility function to get nested property
			function dig(obj, selector) {
				let result = obj;
				const splitter = selector.split(".");
				for (let i = 0; i < splitter.length; i++) {
					if (typeof result === "undefined" || result === null) {
						return undefined;
					}
					result = result[splitter[i]];
				}
				return result;
			}

			if (typeof field === "function") return field(obj);
			if (typeof field === "string") return dig(obj, field);
			return undefined;
		},

		collectFormatted(obj, column, headerRow = false) {
			let value;
			if (headerRow && column.headerField) {
				value = this.collect(obj, column.headerField);
			} else {
				value = this.collect(obj, column.field);
			}
			if (value === undefined) return "";

			// if user has supplied custom formatter,
			// use that here
			if (column.formatFn && typeof column.formatFn === "function") {
				return column.formatFn(value, obj);
			}

			// lets format the resultant data
			let type = column.typeDef;
			// this will only happen if we try to collect formatted
			// before types have been initialized. for example: on
			// load when external query is specified.
			if (!type) {
				type = this.dataTypes[column.type] || defaultType;
			}

			let result = type.format(value, column);
			// we must have some values in compact mode
			if (this.compactMode && (result == "" || result == null)) return "-";
			return result;
		},

		formattedRow(row, isHeaderRow = false) {
			const formattedRow = {};
			for (let i = 0; i < this.typedColumns.length; i++) {
				const col = this.typedColumns[i];
				// what happens if field is
				if (col.field) {
					formattedRow[col.field] = this.collectFormatted(
						row,
						col,
						isHeaderRow
					);
				}
			}
			return formattedRow;
		},

		// Get classes for the given column index & element.
		getClasses(index, element, row) {
			const { typeDef, [`${element}Class`]: custom } = this.typedColumns[index];
			let { isRight } = typeDef;
			if (this.rtl) isRight = true;

			const classes = {
				"vgt-right-align": isRight,
				"vgt-left-align": !isRight,
			};

			// for td we need to check if value is
			// a function.
			if (typeof custom === "function") {
				classes[custom(row)] = true;
			} else if (typeof custom === "string") {
				classes[custom] = true;
			}
			return classes;
		},

		// method to filter rows
		filterRows(columnFilters, fromFilter = true) {
			// if (!this.rows.length) return;
			// this is invoked either as a result of changing filters
			// or as a result of modifying rows.
			this.columnFilters = columnFilters;
			let computedRows = JSON.parse(JSON.stringify(this.originalRows));
			let instancesOfFiltering = false;

			// do we have a filter to care about?
			// if not we don't need to do anything
			if (this.columnFilters && Object.keys(this.columnFilters).length) {
				// every time we filter rows, we need to set current page
				// to 1
				// if the mode is remote, we only need to reset, if this is
				// being called from filter, not when rows are changing
				if (this.mode !== "remote" || fromFilter) {
					this.changePage(1);
				}
				// we need to emit an event and that's that.
				// but this only needs to be invoked if filter is changing
				// not when row object is modified.
				if (fromFilter) {
					this.$emit("column-filter", {
						columnFilters: this.columnFilters,
					});
				}

				// if mode is remote, we don't do any filtering here.
				if (this.mode === "remote") {
					if (fromFilter) {
						this.$emit("update:isLoading", true);
					} else {
						// if remote filtering has already been taken care of.
						this.filteredRows = computedRows;
					}
					return;
				}

				const fieldKey = (field) => {
					if (typeof field === "function" && field.name) {
						return field.name;
					}
					return field;
				};

				for (let i = 0; i < this.typedColumns.length; i++) {
					const col = this.typedColumns[i];
					if (this.columnFilters[fieldKey(col.field)]) {
						instancesOfFiltering = true;
						computedRows.forEach((headerRow) => {
							const newChildren = headerRow.children.filter((row) => {
								// If column has a custom filter, use that.
								if (
									col.filterOptions &&
									typeof col.filterOptions.filterFn === "function"
								) {
									return col.filterOptions.filterFn(
										this.collect(row, col.field),
										this.columnFilters[fieldKey(col.field)]
									);
								}

								// Otherwise Use default filters
								const { typeDef } = col;
								return typeDef.filterPredicate(
									this.collect(row, col.field),
									this.columnFilters[fieldKey(col.field)],
									false,
									col.filterOptions &&
										typeof col.filterOptions.filterDropdownItems === "object"
								);
							});
							// should we remove the header?
							headerRow.children = newChildren;
						});
					}
				}
			}

			if (instancesOfFiltering) {
				this.filteredRows = computedRows.filter(
					(h) => h.children && h.children.length
				);
			} else {
				this.filteredRows = computedRows;
			}
		},

		getCurrentIndex(rowId) {
			let index = 0;
			let found = false;
			for (let i = 0; i < this.paginated.length; i += 1) {
				const headerRow = this.paginated[i];
				const { children } = headerRow;
				if (children && children.length) {
					for (let j = 0; j < children.length; j += 1) {
						const c = children[j];
						if (c.originalIndex === rowId) {
							found = true;
							break;
						}
						index += 1;
					}
				}
				if (found) break;
			}
			return (this.currentPage - 1) * this.currentPerPage + index + 1;
		},

		getRowStyleClass(row) {
			let classes = "";
			if (this.hasRowClickListener) classes += "clickable";
			let rowStyleClasses;
			if (typeof this.rowStyleClass === "function") {
				rowStyleClasses = this.rowStyleClass(row);
			} else {
				rowStyleClasses = this.rowStyleClass;
			}
			if (rowStyleClasses) {
				classes += ` ${rowStyleClasses}`;
			}

			if (this.expandedRowIndex === row.originalIndex) {
				classes += ` ${this.expandedRowClasses}`;
			}

			return classes;
		},

		handleGrouped(originalRows) {
			originalRows.forEach((headerRow, i) => {
				headerRow.vgt_header_id = i;
				if (
					this.groupOptions.maintainExpanded &&
					this.expandedRowKeys.has(headerRow[this.groupOptions.rowKey])
				) {
					headerRow["vgtIsExpanded"] = true;
				}
				headerRow.children.forEach((childRow) => {
					childRow.vgt_id = i;
				});
			});
			return originalRows;
		},

		initializePagination() {
			const {
				enabled,
				perPage,
				position,
				perPageDropdown,
				perPageDropdownEnabled,
				dropdownAllowAll,
				nextLabel,
				prevLabel,
				rowsPerPageLabel,
				ofLabel,
				pageLabel,
				allLabel,
				setCurrentPage,
				mode,
				infoFn,
			} = this.paginationOptions;

			if (typeof enabled === "boolean") {
				this.paginate = enabled;
			}

			if (typeof perPage === "number") {
				this.perPage = perPage;
			}

			if (position === "top") {
				this.paginateOnTop = true; // default is false
				this.paginateOnBottom = false; // default is true
			} else if (position === "both") {
				this.paginateOnTop = true;
				this.paginateOnBottom = true;
			}

			if (Array.isArray(perPageDropdown) && perPageDropdown.length) {
				this.customRowsPerPageDropdown = perPageDropdown;
				if (!this.perPage) {
					[this.perPage] = perPageDropdown;
				}
			}

			if (typeof perPageDropdownEnabled === "boolean") {
				this.perPageDropdownEnabled = perPageDropdownEnabled;
			}

			if (typeof dropdownAllowAll === "boolean") {
				this.paginateDropdownAllowAll = dropdownAllowAll;
			}

			if (typeof mode === "string") {
				this.paginationMode = mode;
			}

			if (typeof nextLabel === "string") {
				this.nextText = nextLabel;
			}

			if (typeof prevLabel === "string") {
				this.prevText = prevLabel;
			}

			if (typeof rowsPerPageLabel === "string") {
				this.rowsPerPageText = rowsPerPageLabel;
			}

			if (typeof ofLabel === "string") {
				this.ofText = ofLabel;
			}

			if (typeof pageLabel === "string") {
				this.pageText = pageLabel;
			}

			if (typeof allLabel === "string") {
				this.allText = allLabel;
			}

			if (typeof setCurrentPage === "number") {
				setTimeout(() => {
					this.changePage(setCurrentPage);
				}, 500);
			}

			if (typeof infoFn === "function") {
				this.paginationInfoFn = infoFn;
			}
		},

		initializeExpandRows() {
			const { enabled } = this.expandRowsOptions;

			if (typeof enabled === "boolean") {
				this.expandRowsEnabled = enabled;
			}
		},

		initializeSearch() {
			const {
				enabled,
				trigger,
				externalQuery,
				searchFn,
				placeholder,
				skipDiacritics,
			} = this.searchOptions;

			if (typeof enabled === "boolean") {
				this.searchEnabled = enabled;
			}

			if (trigger === "enter") {
				this.searchTrigger = trigger;
			}

			if (typeof externalQuery === "string") {
				this.externalSearchQuery = externalQuery;
			}

			if (typeof searchFn === "function") {
				this.searchFn = searchFn;
			}

			if (typeof placeholder === "string") {
				this.searchPlaceholder = placeholder;
			}

			if (typeof skipDiacritics === "boolean") {
				this.searchSkipDiacritics = skipDiacritics;
			}
		},

		initializeSort() {
			const { enabled, initialSortBy, multipleColumns } = this.sortOptions;
			const initSortBy = JSON.parse(JSON.stringify(initialSortBy || {}));

			if (typeof enabled === "boolean") {
				this.sortable = enabled;
			}

			if (typeof multipleColumns === "boolean") {
				this.multipleColumnSort = multipleColumns;
			}

			//* initialSortBy can be an array or an object
			if (typeof initSortBy === "object") {
				const ref = this.fixedHeader
					? this.$refs["table-header-secondary"]
					: this.$refs["table-header-primary"];
				if (Array.isArray(initSortBy)) {
					ref.setInitialSort(initSortBy);
				} else {
					const hasField = Object.prototype.hasOwnProperty.call(
						initSortBy,
						"field"
					);
					if (hasField) ref.setInitialSort([initSortBy]);
				}
			}
		},

		initializeSelect() {
			const {
				enabled,
				selectionInfoClass,
				selectionText,
				clearSelectionText,
				selectOnCheckboxOnly,
				selectAllByPage,
				disableSelectInfo,
				selectAllByGroup,
			} = this.selectOptions;

			if (typeof enabled === "boolean") {
				this.selectable = enabled;
			}

			if (typeof selectOnCheckboxOnly === "boolean") {
				this.selectOnCheckboxOnly = selectOnCheckboxOnly;
			}

			if (typeof selectAllByPage === "boolean") {
				this.selectAllByPage = selectAllByPage;
			}

			if (typeof selectAllByGroup === "boolean") {
				this.selectAllByGroup = selectAllByGroup;
			}

			if (typeof disableSelectInfo === "boolean") {
				this.disableSelectInfo = disableSelectInfo;
			}

			if (typeof selectionInfoClass === "string") {
				this.selectionInfoClass = selectionInfoClass;
			}

			if (typeof selectionText === "string") {
				this.selectionText = selectionText;
			}

			if (typeof clearSelectionText === "string") {
				this.clearSelectionText = clearSelectionText;
			}
		},
	},

	mounted() {
		if (this.perPage) {
			this.currentPerPage = this.perPage;
		}
		this.initializeSort();
	},

	components: {
		"vgt-pagination": VgtPagination,
		"vgt-global-search": VgtGlobalSearch,
		"vgt-header-row": VgtHeaderRow,
		"vgt-table-header": VgtTableHeader,
	},
};
</script>

<style lang="scss">
@import "../styles/style";
</style>
